﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using FarseerPhysics.Common;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Factories;
using lumo;
using lumo.display.light;
using lumo.display.object2D;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Media;

namespace game.character
{
    /// <summary>
    /// Class containing all map objects.
    /// </summary>
    public class GameMap : IUpdatable
    {
        //-- Public properties
        public World World { get; protected set; }
        public LightManager LightManager { get; protected set; }
        public Tiled Tiled { get; protected set; }
        public GamePlayer Player { get; protected set; }
        public List<GameEvent> Events { get; protected set; }
        public List<Body> Bodies { get; protected set; }
        
        public uint Width { get { return Tiled.Width; } }
        public uint Height { get { return Tiled.Height; } }
        public uint Depth { get { return (uint)Tiled.LayerList.Count; } }

        //-- Private variables
        private Dictionary<string, Action<Tiled.Object, LumoComponent>> ObjectHandlerActionDictionary = new Dictionary<string, Action<Tiled.Object, LumoComponent>>();
        
        /// <summary>
        /// Game Map constructor.
        /// </summary>
        /// <param name="component"></param>
        public GameMap(LumoComponent component)
        {
            // Create objects.
            World = new World(new Vector2(0.0f, 0.25f));
            LightManager = new LightManager(component, "Effects/Light/Linearize", "Effects/Light/Delinearize", 128, 128);
            Tiled = null;
            Player = new GamePlayer(component, this);
            Events = new List<GameEvent>();
            Bodies = new List<Body>();
            
            // Setup action handlers
            ObjectHandlerActionDictionary["solid"]  = SetupSolidObject;
            ObjectHandlerActionDictionary["start"]  = SetupStartObject;
            ObjectHandlerActionDictionary["sensor"] = SetupSensorObject;
            ObjectHandlerActionDictionary["enemy"]  = SetupEnemyObject;
            ObjectHandlerActionDictionary["item"]   = SetupItemObject;
        }

        /// <summary>
        /// Setup the game map.
        /// </summary>
        /// <param name="component"></param>
        /// <param name="tiled"></param>
        /// <returns></returns>
        public bool Setup(LumoComponent component, Tiled tiled = null)
        {
            foreach ( Body body in Bodies )
                World.RemoveBody(body);
            Bodies.Clear();
            Events.Clear();
            Tiled = tiled;
            if (tiled == null)
                return false;

            // Reset the player.
            Player.Setup();

            // For each layer.
            foreach (Tiled.Layer layer in Tiled.Layers)
            {
                // Layer is not an object group.
                if (!(layer is Tiled.ObjectGroup))
                    continue;

                // The Group
                Tiled.ObjectGroup group = layer as Tiled.ObjectGroup;

                // For each object
                foreach (Tiled.Object obj in group.Objects)
                {
                    // Invalid object type.
                    if (obj.Type == null)
                        continue;

                    // Lower type
                    string LowerType = obj.Type.ToLower();

                    // Iterate through each
                    foreach (var pair in ObjectHandlerActionDictionary)
                        if (LowerType == pair.Key.ToLower())
                            pair.Value(obj, component);
                }
            }

            // Set the Light Manager ambient color.
            LightManager.Color = new Color(0.05f, 0.05f, 0.05f, 0.05f);

            // Set the position of the camera.
            Vector2 position = new Vector2( 
                Player.Position.X - (Global.DefaultScreenSize.X / 2), 
                Player.Position.Y - (Global.DefaultScreenSize.Y / 2)
            );
            Global.Camera.SetTarget(position);
            Global.Camera.Position = position;

            // Successfully setuped.
            return true;
        }

        /// <summary>
        /// Setup a solid object.
        /// </summary>
        /// <param name="obj"> The object to setup </param>
        private void SetupSolidObject(Tiled.Object obj, LumoComponent component)
        {
            // The body
            Body body = null;

            // Point Square object.
            if (obj.GeometryType == Tiled.Object.GeometryTypes.Point)
            {
                // Invalid object.
                if ((obj.Width <= 0) || (obj.Height <= 0))
                    return;

                body = BodyFactory.CreateRectangle(World, obj.Width / Global.PhysicsScale, obj.Height / Global.PhysicsScale, 1.0f,
                    new Vector2((obj.X + obj.Width / 2) / Global.PhysicsScale, (obj.Y + obj.Height / 2) / Global.PhysicsScale));
            }
            // Polygon object.
            else if (obj.GeometryType == Tiled.Object.GeometryTypes.Polygon)
            {
                if (obj.Points.Count <= 0)
                    return;

                Vertices vertices = new Vertices();
                foreach (Vector2 vec in obj.Points)
                    vertices.Add(Vector2.Divide(vec, Global.PhysicsScale));

                body = BodyFactory.CreateLoopShape(World, vertices, Vector2.Divide(
                    new Vector2(obj.X, obj.Y), Global.PhysicsScale));
            }
            // Line object.
            else if (obj.GeometryType == Tiled.Object.GeometryTypes.Line)
            {
                if (obj.Points.Count <= 0)
                    return;

                body = BodyFactory.CreateBody(World, Vector2.Divide
                    (new Vector2(obj.X, obj.Y), Global.PhysicsScale));
                for (int i = 0; i < obj.Points.Count - 1; ++i)
                    FixtureFactory.AttachEdge(Vector2.Divide(obj.Points[i], Global.PhysicsScale),
                        Vector2.Divide(obj.Points[i + 1], Global.PhysicsScale), body, null);
            }

            // Add the body.
            if (body != null)
            {
                body.BodyType = BodyType.Static;
                body.CollisionCategories = Global.PhysicsCategory.Level;

                Bodies.Add(body);
            }
        }

        /// <summary>
        /// Setup the "start" object.
        /// </summary>
        /// <param name="obj"> The object to setup </param>
        private void SetupStartObject(Tiled.Object obj, LumoComponent component)
        {
            // Position of the player.
            Player.Position = new Vector2(obj.X, obj.Y);

            // Check for camera.
            if ((obj.Properties as IDictionary).Contains("Camera"))
            {
                // Match a regex for the camera.
                Match match = Regex.Match(
                    obj.Properties["Camera"],
                    @"\([ ]*?(\d+)[ ]*?,[ ]*?(\d+)[ ]*?,[ ]*?(\d+)[ ]*?,[ ]*?(\d+)[ ]*?\)"
                );
                // On success
                if (match.Success)
                {
                    Global.Camera.SetBoundaries(
                        new Rectangle(
                            Int32.Parse(match.Groups[1].Value),
                            Int32.Parse(match.Groups[2].Value),
                            Int32.Parse(match.Groups[3].Value),
                            Int32.Parse(match.Groups[4].Value)
                        )
                    );
                }
            }
        }

        /// <summary>
        /// Setup the "start" object.
        /// </summary>
        /// <param name="obj"> The object to setup </param>
        private void SetupSensorObject(Tiled.Object obj, LumoComponent component)
        {
            // Create the sensor body.
            Body body = BodyFactory.CreateRectangle(
                World,
                obj.Width / Global.PhysicsScale,
                obj.Height / Global.PhysicsScale,
                1.0f,
                new Vector2(
                    (obj.X + obj.Width / 2) / Global.PhysicsScale,
                    (obj.Y + obj.Height / 2) / Global.PhysicsScale
                )
            );

            // Setup the body.
            body.BodyType = BodyType.Static;
            body.IsSensor = true;
            body.CollisionCategories = Global.PhysicsCategory.Sensor;
            body.CollidesWith = Global.PhysicsCategory.Actor;

            bool Valid = false;
            bool[] Locked = new bool[] { false };
            if ((obj.Properties as IDictionary).Contains("Camera"))
            {
                // The to property for the camera
                string to = obj.Properties["Camera"];

                Rectangle? Rect = null;

                // Check for camera properties.
                if (to.ToLower() == "self")
                {
                    Rect = new Rectangle(obj.X, obj.Y, obj.Width, obj.Height);
                }
                else
                {
                    // Try to match
                    Match match = Regex.Match(
                        to,
                        @"\([ ]*?(\d+)[ ]*?,[ ]*?(\d+)[ ]*?,[ ]*?(\d+)[ ]*?,[ ]*?(\d+)[ ]*?\)"
                    );

                    // Try to match
                    if (match.Success)
                    {
                        Rect = new Rectangle(
                            Int32.Parse(match.Groups[1].Value),
                            Int32.Parse(match.Groups[2].Value),
                            Int32.Parse(match.Groups[3].Value),
                            Int32.Parse(match.Groups[4].Value)
                        );
                    }
                }
                if (Rect != null)
                {
                    body.OnCollision += (fixa, fixb, contact) =>
                    {
                        if (!Locked[0])
                            Global.Camera.AddBoundaries(Rect);
                        return true;
                    }; 
                    body.OnSeparation += (fixa, fixb) =>
                    {
                        Global.Camera.RemoveBoundaries(Rect);
                    };
                    Valid = true;
                }
            }

            // Camera sensor.
            if ((obj.Properties as IDictionary).Contains("Song"))
            {
                body.OnCollision += (fixa, fixb, contact) =>
                {
                    MediaPlayer.Play(component.Content.Load<Song>(obj.Properties["Song"]));
                    MediaPlayer.IsRepeating = true;
                    return true;
                };
                Valid = true;
            }
            // Camera sensor.
            if ((obj.Properties as IDictionary).Contains("OutLock"))
            {
                body.OnSeparation += (fixa, fixb) =>
                {
                    Locked[0] = true;
                    body.IsSensor = false;
                    body.CollisionCategories = Global.PhysicsCategory.Level;
                };
                Valid = true;
            }
            if (!Valid)
                World.RemoveBody(body);
        }
        /// <summary>
        /// Setup the "enemy" object.
        /// </summary>
        /// <param name="obj"> The object to setup </param>
        private void SetupEnemyObject(Tiled.Object obj, LumoComponent component)
        {
            if ((obj.Properties as IDictionary).Contains("Class"))
            {
                string ClassName = obj.Properties["Class"];

                Type EnemyClassType = Type.GetType("game.character.enemy." + ClassName);
                if (EnemyClassType == null)
                    EnemyClassType = Type.GetType(ClassName);
                if (EnemyClassType == null)
                    return;
                object evt = Activator.CreateInstance(EnemyClassType, new object[] { component, this, obj });
                if (evt is GameEvent)
                {
                    GameEvent gevt = evt as GameEvent;
                    gevt.Setup();
                    Events.Add(gevt);
                }
            }
        }

        /// <summary>
        /// Setup the "enemy" object.
        /// </summary>
        /// <param name="obj"> The object to setup </param>
        private void SetupItemObject(Tiled.Object obj, LumoComponent component)
        {
            if ((obj.Properties as IDictionary).Contains("Class"))
            {
                string ClassName = obj.Properties["Class"];

                Type EnemyClassType = Type.GetType("game.character.item." + ClassName);
                if (EnemyClassType == null)
                    EnemyClassType = Type.GetType(ClassName);
                if (EnemyClassType == null)
                    return;
                object evt = Activator.CreateInstance(EnemyClassType, new object[] { component, this, obj });
                if (evt is GameEvent)
                {
                    GameEvent gevt = evt as GameEvent;
                    gevt.Setup();
                    Events.Add(gevt);
                }
            }
        }

        /// <summary>
        /// Update the map.
        /// </summary>
        /// <param name="component">The component to update everything.</param>
        public void Update(LumoComponent component)
        {
            Global.Camera.Update(component);
            Global.Camera.SetTarget(new Vector2(Player.Position.X - (Global.DefaultScreenSize.X / 2), Player.Position.Y - (Global.DefaultScreenSize.Y / 2)));

            foreach (GameEvent evt in Events)
                evt.Update(component);
            Events.RemoveAll(evt => evt.Deleted);
            Player.Update(component);
        }

    }

}